package edu.drexel.cs544.group11.server;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Vector;

import edu.drexel.cs544.group11.common.MessageFormatException;
import edu.drexel.cs544.group11.common.Product;
import edu.drexel.cs544.group11.common.ProtocolState;
import edu.drexel.cs544.group11.common.RequestCommand;
import edu.drexel.cs544.group11.common.RequestMessage;
import edu.drexel.cs544.group11.common.ResponseMessage;
import edu.drexel.cs544.group11.common.ResponseStatusCode;
import edu.drexel.cs544.group11.serverpersistence.ProductPersistence;

/**
 * STATEFUL
 * 
 * Represents a BSP server.
 * 
 * CS 544
 * 
 * @author Group 11:
 * @author Jason Setzer
 * @author Tam Thongsima
 * @author Kenny Oyefara
 * @author Chase Miller
 * 
 * @date Spring 2013
 * 
 */
public class Server {
    private Vector<ClientConnection> clients;
    private ProductPersistence productPersistence;
    private static double version = 1.0;
    
    /**
     * Creates a new server instance.
     */
    public Server(ProductPersistence productPersistence) {
	this.clients = new Vector<ClientConnection>();
	this.productPersistence = productPersistence;
    }
    
    /**
     * Creates a new client connection and adds it to the server's list of
     * connections.
     * 
     * @param socket - the Socket connection.
     */
    private void addClient(ClientConnection connection) {
	//allocate a client id and add to the list of client
	connection.setClientId(this.getNextClientConnectionId());
	connection.setState(ProtocolState.SECURE_READY);
	this.clients.add(connection);
    }
    
    /**
     * Answers sending a success response to the specified client.
     * 
     * @param connection - the client connection
     */
    public void sendSuccess(ClientConnection connection) {
	// TODO - replace 1 with correct value
	ResponseMessage response = new ResponseMessage(ResponseStatusCode.SUCCESS, 1, version);
	try {
	    response.sendMessage(connection.getPrintStream());
	}
	catch (IOException | MessageFormatException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	}
    }
    
    /**
     * Initiates a new connection.
     * 
     * CONCURRENT
     * 
     * @param connection
     */
    public void init(ClientConnection connection) {
	//check if is on the list if not add them
	if (!this.clients.contains(connection)) {
	    this.addClient(connection);
	    connection.start(); // CONCURRENT
	}
	else {
	    return;
	}
    }
    
    /**
     * STATEFUL
     * 
     * Handles the specified request.
     * 
     * @param callingClient - the ClientConnection of the client making the
     *            request
     * @param message - the request message
     * @throws IOException
     */
    public synchronized void handleRequest(ClientConnection callingClient, RequestMessage message) throws IOException {
	OutputStream outgoingStream = callingClient.getClientSocket().getOutputStream();
	int messageSeqId = message.getMessageSeqId();
	if (message.getCommand() == RequestCommand.END_SESSION) {
	    this.endSession(callingClient);
	}
	else {
	    String text = null;
	    
	    switch (callingClient.getProtocolState()) {
		case SECURE_READY:
		    if (PerformSecureReadyStateCommand(callingClient, message))
			Success(messageSeqId, outgoingStream);
		    else
			fail(messageSeqId, outgoingStream);
		    break;
		case ADMIN_SESSION:
		    if ((text = PerformAdminCommand(callingClient, message)) != null) {
			Success(messageSeqId, outgoingStream, text);
		    }
		    else
			fail(messageSeqId, outgoingStream);
		    break;
		case CUSTOMER_SESSION:
		    if (PerformCustomerSessionStateCommand(callingClient, message))
			Success(messageSeqId, outgoingStream);
		    else
			fail(messageSeqId, outgoingStream);
		    break;
		case PRICE_CHECK_SESSION:
		    if ((text = PerformPriceCheckStateCommand(callingClient, message)) != null)
			Success(messageSeqId, outgoingStream, text);
		    else
			fail(messageSeqId, outgoingStream);
		    break;
		case PURCHASING:
		    if (PerformPurchasingCommand(callingClient, message))
			Success(messageSeqId, outgoingStream);
		    else
			fail(messageSeqId, outgoingStream);
		    break;
		case CART_SCAN:
		    if ((text = PerformCartScanCommand(callingClient, message)) != null)
			Success(messageSeqId, outgoingStream, text);
		    else
			fail(messageSeqId, outgoingStream);
		    break;
		default:
		    this.fail(messageSeqId, outgoingStream);// no such state
		    break;
	    }
	    
	}
    }
    
    /**
     * STATEFUL
     * 
     * This function gets called to perform purchasing state's commands
     * 
     * @param callingClient
     * @param message
     * @return
     */
    private boolean PerformPurchasingCommand(ClientConnection callingClient, RequestMessage message) {
	RequestCommand command = message.getCommand();
	switch (command) {
	    case PAY:
		return ProcessPayment(callingClient, message);
	    case TO_SECURE_READY:
		callingClient.setState(ProtocolState.SECURE_READY);
		return true;
	    case TO_CUSTOMER_SESSION:
		callingClient.setState(ProtocolState.CUSTOMER_SESSION);
		return true;
	    default:
		return false;//no such command
	}
    }
    
    /**
     * STATEFUL
     * 
     * Performs cart scan command
     * 
     * @param callingClient
     * @param message
     * @return - null if unsuccessful.
     * @throws IOException
     */
    private String PerformCartScanCommand(ClientConnection callingClient, RequestMessage message) throws IOException {
	RequestCommand command = message.getCommand();
	switch (command) {
	    case ADD_ITEM:
		return (addProduct(callingClient.getClientCart(), message) == false) ? null : "";
	    case SCAN_FOR_ITEM_INFO:
		return ReadInventoryItem(message);
	    case REMOVE_ITEM:
		return (removeProduct(callingClient, message) == false) ? null : "";
	    case TO_CUSTOMER_SESSION:
		callingClient.setState(ProtocolState.CUSTOMER_SESSION);
		return "";
	    default:
		return null;//no such command
	}
    }
    
    /**
     * STATEFUL
     * 
     * This function performs price check state commands
     * 
     * @param callingClient
     * @param message
     * @return
     */
    
    private String PerformPriceCheckStateCommand(ClientConnection callingClient, RequestMessage message) {
	RequestCommand command = message.getCommand();
	switch (command) {
	    case TO_SECURE_READY:
		callingClient.setState(ProtocolState.SECURE_READY);
		return "";
	    case SCAN_FOR_ITEM_INFO:
		return ReadInventoryItem(message);
	    default:
		return null;
	}
    }
    
    /**
     * Ends a client session.
     * 
     * @param callingClient
     */
    private void endSession(ClientConnection callingClient) {
	this.clients.remove(callingClient);
	callingClient.stop();
    }
    
    /**
     * Returns the next valid client id (highest plus one).
     * 
     * @return
     */
    private synchronized int getNextClientConnectionId() {
	int highest = 0;
	for (ClientConnection next : this.clients) {
	    int thisId = next.getClientId();
	    if (thisId >= highest) {
		highest = thisId + 1;
	    }
	}
	return highest;
    }
    
    /**
     * Does everything necessary to indicate a fail.
     */
    private void fail(int messageId, OutputStream out) {
	ResponseMessage serverResponse = new ResponseMessage(ResponseStatusCode.FAIL, messageId, version);
	sendServerResponse(serverResponse, new PrintStream(out));
    }
    
    /**
     * send success message
     * 
     * @param messageId
     * @param out
     */
    private void Success(int messageId, OutputStream out) {
	ResponseMessage serverResponse = new ResponseMessage(ResponseStatusCode.SUCCESS, messageId, version);
	sendServerResponse(serverResponse, new PrintStream(out));
    }
    
    /**
     * Send a success message.
     * 
     * @param messageId
     * @param out
     * @param text
     */
    private void Success(int messageId, OutputStream out, String text) {
	if (text.equals("")) {
	    this.Success(messageId, out);
	}
	else {
	    ResponseMessage serverResponse = new ResponseMessage(ResponseStatusCode.SUCCESS, messageId, version, text);
	    sendServerResponse(serverResponse, new PrintStream(out));
	}
    }
    
    private void sendServerResponse(ResponseMessage serverResponse, PrintStream outGoing) {
	try {
	    serverResponse.sendMessage(outGoing);
	}
	catch (IOException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	}
	catch (MessageFormatException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	}
    }
    
    /**
     * Updates the specified client connection's state with the specified state.
     * 
     * @param connection
     * @param state
     */
    private void updateClientState(ClientConnection connection, ProtocolState state) {
	connection.setState(state);
    }
    
    /**
     * STATEFUL Performs everything necessary to process a new order request. 1.
     * creates a list that product items will be added to.
     * 
     * @param connection
     */
    private boolean performNewOrder(ClientConnection callingClient) {
	Vector<Product> clientCart = new Vector<Product>();
	callingClient.setClientCart(clientCart);
	this.updateClientState(callingClient, ProtocolState.CUSTOMER_SESSION);
	return true;// all went well
    }
    
    /**
     * STATEFUL function perform customer session state command
     * 
     * @param callingClient
     * @param message
     * @return
     * @throws IOException
     */
    private boolean PerformCustomerSessionStateCommand(ClientConnection callingClient, RequestMessage message) throws IOException {
	
	RequestCommand command = message.getCommand();
	switch (command) {
	    case TO_PURCHASING:
		callingClient.setState(ProtocolState.PURCHASING);
		return true;
	    case TO_CART_SCAN:
		callingClient.setState(ProtocolState.CART_SCAN);
		return true;
	    case TO_SECURE_READY:
		callingClient.setState(ProtocolState.SECURE_READY);
		return true;
	    default:
		return false;//no such command
	}
    }
    
    private boolean checkoutOrder(ClientConnection callingClient, RequestMessage message) {
	if (getToTalPrice(callingClient, message)) {
	    System.out.println("Receipt Number: ATHU" + Math.random());
	    return true;
	}
	else
	    return false;
    }
    
    /**
     * gets the total price and quantity of all products in the clients shopping
     * cart
     * 
     * @param callingClient
     * @param message
     * @return
     */
    
    private boolean getToTalPrice(ClientConnection callingClient, RequestMessage message) {
	Iterator<Product> productList = callingClient.getClientCart().iterator();
	double totalPrice = 0;
	int totalQuantity = 0;
	try {
	    while (productList.hasNext()) {
		Product product = productList.next();
		totalPrice = +product.getPrice();
		totalQuantity = +product.getQuantity();
	    }
	    System.out.println("Total price: " + totalPrice + "Total Quantity" + totalQuantity);
	    return true;
	}
	catch (Exception e) {
	    return false;
	}
    }
    
    /**
     * This function cancels an order
     * 
     * @param callingClient
     * @param message
     * @return
     * @throws IOException
     */
    private boolean cancelOrder(ClientConnection callingClient, RequestMessage message) {
	try {
	    this.removeProduct(callingClient, message);
	    System.out.println("Your Order has being successfully canceled, Thanks for your business");
	    return true;
	}
	catch (Exception e) {
	    //delete all items caused errors
	    e.printStackTrace();
	    System.out.println("Your Order can not be canceled, try again later.");
	    return false;
	}
    }
    
    /**
     * Delete from cart.
     * 
     * @return
     * @throws IOException
     */
    private boolean removeProduct(ClientConnection callingClient, RequestMessage message) throws IOException {
	RequestCommand command = message.getCommand();
	if (command == RequestCommand.REMOVE_ITEM) {
	    String code = getBarcode(message);
	    Product product = checkInventory(code);
	    callingClient.getClientCart().removeElement(product);
	    return true;
	}
	else
	    return false;
	
    }
    
    /**
     * this function gets the product from message parameters check if product
     * exist in inventory if true then creates a product and adds that to list
     * if product for that client if product already exist on list increase
     * quantity of that product.
     * 
     * @param message
     * @return
     */
    private boolean addProduct(Vector<Product> clientCart, RequestMessage message) {
	
	String code = getBarcode(message);
	
	Product product = checkInventory(code);
	if (product != null) {
	    if (clientCart.contains(product)) {
		Product existingProduct = clientCart.elementAt(clientCart.lastIndexOf(product));
		existingProduct.incrementQuantity();
	    }
	    else {
		//add new item to list
		clientCart.add(product);
	    }
	    return true;
	}
	return false;
	
    }
    
    /**
     * 
     * @param Barcode
     * @return Product found in inventory if available else null
     */
    private Product checkInventory(String Barcode) {
	return this.productPersistence.ReadFromBarcode(Barcode);
    }
    
    /**
     * Get barcode string from body.
     * 
     * @param message
     * @return
     */
    private String getBarcode(RequestMessage message) {
	String messageBody = message.getMessageBody();
	return messageBody;
    }
    
    /**
     * STATEFUL
     * 
     * This function process payment information the only valid card number
     * "123456"
     * 
     * @return true
     */
    private boolean ProcessPayment(ClientConnection callingClient, RequestMessage message) {
	String messageBody = message.getMessageBody();
	int indexOfCardNumber = messageBody.indexOf("number:");
	int indexOfCardName = messageBody.indexOf(", name:");
	int indexOfCardType = messageBody.indexOf(", vendor:");
	int indexOfCardExp = messageBody.indexOf(", exp:");
	String cardNumber = messageBody.substring(indexOfCardNumber + 7, indexOfCardNumber + 23);
	String cardName = messageBody.substring(indexOfCardName + 7, indexOfCardType);
	String cardVendor = messageBody.substring(indexOfCardType + 9, indexOfCardExp);
	String cardexp = messageBody.substring(indexOfCardExp + 6);
	if (cardNumber.isEmpty() || cardName.isEmpty() || cardVendor.isEmpty() || cardexp.isEmpty()) {
	    return false;
	}
	else {
	    callingClient.setState(ProtocolState.SECURE_READY);
	    return true;
	}
    }
    
    /**
     * STATEFUL
     * 
     * Performs Admin Session State command
     * 
     * @param callingClient
     * @param message
     * @return
     */
    private String PerformAdminCommand(ClientConnection callingClient, RequestMessage message) {
	RequestCommand command = message.getCommand();
	String messageBody = message.getMessageBody();
	switch (command) {
	    case TO_SECURE_READY:
		callingClient.setState(ProtocolState.SECURE_READY);
		return "";
	    case UPDATE_INVENTORY_ITEM:
		return (UpdateInventoryItem(messageBody) == false) ? null : "";
	    case ADD_INVENTORY_ITEM:
		return (AddInventoryItem(messageBody) == false) ? null : "";
	    case SCAN_FOR_ITEM_INFO:
		return ReadInventoryItem(message);
	    default:
		return null;
	}
	
    }
    
    /**
     * messageBody = "123456789999"; Read Product info from inventory.
     * 
     * @param message
     * @return - text if item was found; otherwise null.
     */
    private String ReadInventoryItem(RequestMessage message) {
	String messageBody = this.getBarcode(message);
	Product product = this.productPersistence.ReadFromBarcode(messageBody);
	
	String textToReturn = null;
	
	if (product != null) {
	    textToReturn = product.toString();
	}
	
	return textToReturn;
    }
    
    /**
     * Read message and update product in inventory file Element can be price,
     * quantity, name, category
     * 
     * @param messageBody : example
     *            "Barcode:123456789999, Element:category, Value:test test"
     * @return
     */
    private boolean UpdateInventoryItem(String messageBody) {
	
	String barcode = "";
	String element = "";
	String value = "";
	
	try {
	    barcode = messageBody.split("Barcode:")[1].split(",")[0];
	}
	catch (Throwable t) {
	}
	
	try {
	    element = messageBody.split("Element:")[1].split(",")[0];
	}
	catch (Throwable t) {
	}
	
	try {
	    value = messageBody.split("Value:")[1].split(",")[0];
	}
	catch (Throwable t) {
	}
	
	if (barcode.equals("") | element.equals("") | value.equals("")) {
	    return false;
	}
	
	if (element.equals("price") && !this.isDouble(value)) {
	    return false;
	}
	
	if (element.equals("quantity") && !this.isInteger(value)) {
	    return false;
	}
	
	else {
	    return this.productPersistence.UpdateProductElement(barcode, element, value);
	}
    }
    
    /**
     * Returns true if the specified value is an integer.
     * 
     * @param value
     * @return
     */
    private boolean isInteger(String value) {
	boolean result = false;
	
	try {
	    Integer.parseInt(value);
	    result = true;
	}
	catch (Throwable t) {
	    
	}
	return result;
    }
    
    /**
     * Returns true if the provided value is a double.
     * 
     * @param value
     * @return
     */
    private boolean isDouble(String value) {
	boolean result = false;
	
	try {
	    Double.parseDouble(value);
	    result = true;
	}
	catch (Throwable t) {
	    
	}
	return result;
    }
    
    /**
     * Read RequestMessage and add product to inventory
     * 
     * @param messageBody : example
     *            "Barcode:111222333444, Category:Snack, Name:Beef Jerky, Price:5.49, Quantity:50"
     * @return
     */
    private boolean AddInventoryItem(String messageBody) {
	
	String barcode = "";
	String category = "";
	String productName = "";
	Double price = null;
	Integer quantity = null;
	
	try {
	    barcode = messageBody.split("Barcode:")[1].split(",")[0];
	}
	catch (Throwable t) {
	}
	
	try {
	    category = messageBody.split("Category:")[1].split(",")[0];
	}
	catch (Throwable t) {
	}
	
	try {
	    productName = messageBody.split("Name:")[1].split(",")[0];
	}
	catch (Throwable t) {
	}
	
	try {
	    String priceAsString = messageBody.split("Price:")[1].split(",")[0];
	    price = Double.parseDouble(priceAsString);
	}
	catch (Throwable t) {
	}
	
	try {
	    String quantityAsString = messageBody.split("Quantity:")[1].split(",")[0];
	    quantity = Integer.parseInt(quantityAsString);
	}
	catch (Throwable t) {
	}
	
	if (barcode.equals("") || price == null || quantity == null) {
	    return false;
	}
	else {
	    return this.productPersistence.AddProductElement(barcode, category, productName, price, quantity);
	}
    }
    
    /**
     * STATEFUL
     * 
     * Processes the Secure Ready state command
     * 
     * @param callingClient
     * @param message
     */
    private boolean PerformSecureReadyStateCommand(ClientConnection callingClient, RequestMessage message) {
	RequestCommand command = message.getCommand();
	switch (command) {
	    case TO_CUSTOMER_SESSION:
		return this.performNewOrder(callingClient);
	    case TO_ADMIN_SESSION:
		return validateEmployee(callingClient, message);
	    case TO_PRICE_CHECK_SESSION:
		callingClient.setState(ProtocolState.PRICE_CHECK_SESSION);
		return true;
	    default:
		return false;
	}
    }
    
    /**
     * STATEFUL This function validate employee ID from the request message and
     * update the protocol state messageBody : valid list of employee - 22446688
     * 33445577 33445588
     * 
     * @param connection
     * @param message
     * @return
     */
    private boolean validateEmployee(ClientConnection connection, RequestMessage message) {
	String messageBody = message.getMessageBody();
	String id = messageBody.replace("EmployeeID:", "");
	//list of employee
	ArrayList<String> validEmployee = new ArrayList<String>();
	validEmployee.add("22446688");
	validEmployee.add("33445577");
	validEmployee.add("33445588");
	
	if (validEmployee.contains(id)) {
	    this.updateClientState(connection, ProtocolState.ADMIN_SESSION);
	    return true;
	}
	
	return false;
    }
}
